iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
自我挑戰組

React 入門到實作與除錯|30 天哩ㄟ刻系列 第 8

Day 8 : 一觸即發,用事件處理回應操作

  • 分享至 

  • xImage
  •  

全線預備~~ 開始射擊

今天目標

  1. 認識 StrictMode
  2. 認識渲染與事件處理,回應操作的執行時機
  3. 學習卡片加入筆記按鈕與事件
  4. 認識事件冒泡

延續昨天的內容把StrictMode認識完,做一顆按鈕觀察StrictMode,之後進入新的知識點,認識事件冒泡。

認識 StrictMode

StrictMode 會在開發模式對包住的元件啟用額外檢查與警告,讓一些平常不容易重現的問題提早出現,而這些額外行為只在開發模式啟用,不會帶到正式建置。

React:StrictMode

StrictMode 實際做哪些事?

項目 React 行為 檢查對象
渲染的純粹性 額外呼叫元件本體等應保持純粹的函式 渲染時修改原始資料、執行外部動作,導致重複執行後結果改變
Effect 的清理 在應用程式根部啟用時,初次掛載額外執行一輪設定與清理,再設定 忘記取消訂閱、清除計時器或關閉連線,造成資源重複建立
ref 回呼的清理 額外執行回呼的設定與清理流程 沒有移除舊節點參照,造成參照資料持續累積
已棄用 API 對部分舊 API 用法提出警告 程式仍依賴應該更新的舊寫法

例如一個 Effect 負責訂閱事件,正確流程應該能經過「訂閱 → 取消訂閱 → 再訂閱」,最後只保留一份訂閱。如果漏掉取消訂閱,這輪額外執行就可能讓同一個事件被處理兩次,提醒我們補上清理。

React:Fixing bugs found by re-running Effects

這就是 StrictMode 的實際用途:提早暴露重複執行、重新掛載時可能遇到的問題。

關於 Effect 與 ref 的寫法就留到後面吧。

確認專案的 StrictMode 設定

day-1-hello-react 目前安裝的 React Router,預設瀏覽器入口已經用 StrictMode 包住整個應用程式,可以直接沿用。

React Router 框架提供的預設入口檔 entry.client.tsx 的用途與範例可參考這裡 React Router:entry.client.tsx

如果使用的是自己調整過、尚未啟用 StrictMode 的入口,可以先只檢查首頁這一部分。在 app/routes/home.tsx 最上方新增:

import { StrictMode } from "react";

然後保留既有的 RouteWelcome 匯入與 meta 函式,只把 Home 改成:

export default function Home() {
  return (
    <StrictMode> {/* 入口尚未啟用 StrictMode 時,才需要在這裡包裝  */}
      <Welcome />
    </StrictMode>
  );
}

這個局部寫法檢查 Welcome 與它底下的元件;沿用本專案預設入口時,Home 保持原本的 return <Welcome />; 即可,不必重複包。

但框架已經有了,就不用特地做了~

觀察 StrictMode 的額外渲染

app/welcome/welcome.tsxWelcome 函式外面新增:

export function StrictModeProbe() {
  console.log("[觀察:渲染] StrictModeProbe 計算畫面");

  function handleClick() {
    console.log("[觀察:點擊] 按鈕收到一次操作");
  }

  return (
    <section>
      <h1>StrictMode 觀察練習</h1>
      <p>畫面上只放一個觀察元件。</p>
      <button type="button" onClick={handleClick}>
        測試點擊
      </button>
    </section>
  );
}

第一個 Log 放在元件本體,只要 React 呼叫這個元件就會執行。第二個 Log 放在事件處理函式裡,要操作按鈕才會執行。兩個前綴用來區分訊息來源。

原本匯入 Welcome 的那行暫時換成:

import { StrictModeProbe } from "../welcome/welcome";

接著我們替換首頁元件 <Welcome>,改成 <StrictModeProbe>,只修改 Home()

export default function Home() {
{/* 若前面採用局部 StrictMode,只要把 <Welcome /> 換成 <StrictModeProbe />,外層 <StrictMode> 保留 */}
  return <StrictModeProbe />;
}

這一階段讓 Home 只回傳 <StrictModeProbe />。等頁面載入完成後,清空瀏覽器的 Console,關閉保留舊訊息(Preserve log),手動重新載入一次。

會看到元件渲染邏輯被額外執行,但事件處理函式不會被 StrictMode 額外呼叫。
https://ithelp.ithome.com.tw/upload/images/20260922/20184332YUVj0ZypqX.png

「兩筆 Log」是這個簡化的初次載入觀察,不是所有元件永遠只會呼叫兩次的保證,也不能單靠 Log 次數推斷其他專案有沒有啟用 StrictMode。

額外執行為什麼有幫助?

這個正確的情況下,額外呼叫元件仍會得到相同的畫面,所以畫面不會多出第二份。StrictMode 提供的是額外執行的機會,讓我們提早觀察程式能否保持正確。 它不會替我們判斷所有業務結果,也不會自動比較兩次 Log 後宣告程式有錯。

React:Fixing bugs found by double rendering

如果把使用者操作後才應執行的資料寫入放在元件本體,額外渲染就可能重複執行,這能提醒操作造成的動作要放回事件處理函式。

Console 的訊息也要會看:

  • 上面的 [觀察:渲染][觀察:點擊] 都是我們寫的 Log。
  • 部分舊 API 警告可能來自 StrictMode。
  • hydration 不一致訊息則是 React DOM 在接手伺服器 HTML 時回報的內容差異,不以 StrictMode 為必要條件。
    簡單來說就是不能用有沒有紅字,判斷 StrictMode 是否正在運作。

hydration的相關內容:React:hydrateRoot

認識渲染與事件處理,回應操作的執行時機

判斷的關鍵是「這段程式在什麼時候被執行」。 渲染是 React 呼叫元件、計算要呈現的畫面;事件處理則是在點擊等操作發生後,執行對應的處理函式(event handler)。

以剛才的範例來說明,元件本體的 Log 會在渲染時印出,handleClick 裡的 Log 則在操作按鈕時才印出。

程式片段 執行時機 執行內容
console.log("[觀察:渲染]...") React 呼叫 StrictModeProbe 進行渲染時 暫時記錄渲染,方便觀察
return (...) 裡的 JSX 渲染時 產生畫面描述,並把 handleClick 交給 onClick
handleClick 裡的 console.log("[觀察:點擊] …") 使用者操作按鈕,React 呼叫 handleClick 回應這次點擊

接下來回到學習卡片:卡片上的筆記文字會隨著渲染呈現在畫面上;按下新增的按鈕後,事件處理函式才會把那張卡片的筆記印到 Console。

元件函式裡的計算,會隨著渲染執行,事件處理函式(event handler)則交給 React,在對應操作發生時呼叫。

例如,把函式傳給 <button>onClick,就能回應按鈕操作。

React:Responding to Events

先看一個小範例:

function MyButton() {
  function handleClick() {
    console.log("按鈕收到操作了。");
  }

  return (
    <button type="button" onClick={handleClick}>
      按鈕來了
    </button>
  );
}

handleClick 是我們取的函式名稱,習慣上處理事件會用 handle 開頭。接著處理的動作onClick 則是 React 按鈕的事件 prop,要注意大小寫喔。

傳入函式與呼叫函式的差別

上面定義的 handleClick 不會立刻執行它。onClick={handleClick} 是把函式交出去,handleClick() 才是呼叫函式。

意思就是,把 handleClick 這件事情,給 React 處理,當按鈕按下才會觸發事件。

以下幾個組合分別會這樣執行:

寫法 執行方式
onClick={handleClick} 把函式傳給 React,操作按鈕時才呼叫
onClick={() => handleClick()} 傳入箭頭函式,操作時由它呼叫 handleClick()
onClick={handleClick()} 渲染到這裡就先呼叫,並把回傳值當成事件處理函式;此例回傳 void,型別也不符合
onClick={() => handleClick} 操作時只回傳函式本身,沒有呼叫,所以看不到訊息

React:Adding event handlers

學習卡片加入筆記按鈕與事件

app/routes/home.tsx,把 StrictModeProbe 的匯入改回 Welcome,並把元件換回 <Welcome />,我們要把上面講到的,實際應用到 Welcome()內。

  1. 建立筆記按鈕元件。
  2. 新增事件處理函式。
  3. 透過 props 傳事件處理函式給每張卡片的 NoteButton

建立筆記按鈕元件

Welcome() 函式外面新增:

type NoteButtonProps = {
  title: string;
  onRead: () => void;
};

function NoteButton({ title, onRead }: NoteButtonProps) {
  return (
    <button type="button" onClick={onRead}>
      查看「{title}」筆記
    </button>
  );
}

onRead 是自訂元件的 prop 名稱,可以按照用途命名;NoteButton 裡真正的 HTML 按鈕,仍要接到 onClick 才會回應操作。

React:Naming event handler props

新增事件處理函式

接著 Welcome() 函式裡,return 之前,新增處理筆記的函式。LearningPlan 型別沿用昨天的定義。

function handleReadNote(plan: LearningPlan) {
  console.log(`[筆記] ${plan.title}:${plan.note}`);
}

透過 props 傳事件處理函式給每張卡片的 NoteButton

找到 visiblePlans.map((plan) => ...) 裡的卡片,在原本顯示 {plan.note} 的段落後面加入 NoteButton

把按鈕在 <LearningCard></LearningCard> 之間,作為卡片的 children

<p className="text-gray-600">{plan.note}</p>
<NoteButton
  title={plan.title}
  onRead={() => handleReadNote(plan)}
/>

這裡需要把 plan 傳給 handleReadNote,所以用箭頭函式包住呼叫。渲染時先準備函式,等操作按鈕時,才拿對應的計畫執行。直接寫 onRead={handleReadNote(plan)} 就會提早執行。

onRead: () => void 描述的是一個不需要參數、沒有要使用其回傳值的函式,實際執行時,外層的箭頭函式會替它呼叫 handleReadNote(plan)

追蹤按鈕的執行流程

沿著程式的呼叫路徑,可以整理成以下:

操作 NoteButton 裡的按鈕
-> React 呼叫 onClick 收到的 onRead
-> 執行 Welcome 傳入的 () => handleReadNote(plan)
-> Console 印出那張卡片的標題與筆記

認識事件冒泡

Welcome 外面新增 EventDemo。這裡省略樣式,只看內層按鈕和外層區塊各自的事件:

function EventDemo() {
  return (
    <section>
      <h2>事件傳遞練習區</h2>
      <div onClick={() => console.log("[事件] 外層區塊收到點擊")}>
        <p>這裡是外層區塊。</p>
        <button
          type="button"
          onClick={() => console.log("[事件] 內層按鈕收到點擊")}
        >
          測試事件傳遞
        </button>
      </div>
    </section>
  );
}

再到 Welcome 回傳的 JSX,放在最前面,方便操作:

<h1 className="text-2xl font-bold">我的 React 學習卡片</h1>
<EventDemo />

當我們在測試事件傳遞按一下,會一次觸發兩個區塊的事件:
https://ithelp.ithome.com.tw/upload/images/20260922/20184332QivUcZT0jW.png

這是因為內層按鈕與外層區塊各有一個 onClick,當我們點擊內層區塊的觸發按鈕,會再往外層傳遞,稱為事件冒泡(bubbling)

這樣有點麻煩,如果外層也有自己的事件要處理,就會一起被觸發,常見的情境像是表格內的按鈕,跟單元格要處理的事件是分開的,但按下內層的按鈕,外層單元格的事件也動作了。

這時可以利用stopPropagation阻止繼續冒泡,在 EventDemo 裡,只把「測試事件傳遞」按鈕的 onClick 換成:

onClick={(event) => {
  event.stopPropagation();
  console.log("[事件] 內層按鈕收到點擊");
}}

React 會把事件物件(event object)傳給處理函式,這裡取名 event,也常看到寫成 e

event.stopPropagation() 會阻止事件繼續向上冒泡。存檔後清空 Console,再按一次,就會只剩內層按鈕訊息。

寫法 用途
event.stopPropagation() 阻止事件繼續傳遞;這裡讓外層的 onClick 不再收到按鈕的點擊
event.preventDefault() 阻止瀏覽器預設行為,例如連結跳頁或表單提交,不會因此阻止冒泡

React:Stopping propagation
React:Preventing default behavior
事件傳遞還有捕獲階段,可以透過 onClickCapture 處理,看這裡React:Event propagation


為什麽今天沒有錯誤修正練習呢,因為這只是開胃菜,我一開始以為很簡單,直到後面一發不可收拾,才決定要重頭再學一次。/images/emoticon/emoticon70.gif

參考資料


上一篇
Day 7 : 專精一思,讓元件保持純粹
下一篇
Day 9 : 念念不忘,用 state 讓元件記住資料
系列文
React 入門到實作與除錯|30 天哩ㄟ刻9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言